<?php
/**
 * Memcache cluster , failover tolerancy
 * @author Fernando Palomo Garcia fernando@kernelius.com
 * @version 0.1
 * @license Apache License 2.0
 */
class Kernelius_Cache_MemcacheCluster
{

    protected $_server=array();
    protected $_server_config=array();
    protected $_server_up=array();
    protected $_persistent=NULL;


    public function __construct($servers_config){
        if(!is_array($servers_config)){
            throw new exception("SERVERS CONFIG ERROR");
        }

        $i=1;
        foreach($servers_config as $server){
            if(!isset($server["host"]) or !isset($server["port"])){
                throw new exception("ERROR IN SERVER DEFINITION");
            }
            $this->_server_config[$i++]=$server;
        }

        foreach($this->_server_config as $id => $server){
            $cache = new Memcache;
            $this->_server_up[$id]= @$cache->connect( $server["host"], $server["port"]);
            $this->_server[$id]= $cache;
        }
//        var_dump($this);
    }


    /**
     * save key in cache
     * @param string $key
     * @param data $value
     * @param int $timeout
     * @return bool
     */
    public function set($key, $value, $timeout=Kernelius_Cache::DEFAULT_EXPIRY){
        $wrapped_key=$this->wrapKey($key);
        $done=FALSE;
        foreach($this->_getKeyLocation($wrapped_key) as $num => $server){
            Kernelius_Logger::log("set $key($wrapped_key) to cache server($num) {$this->_server_config[$num]["host"]}:{$this->_server_config[$num]["port"]} timeout:$timeout");
            $this_cache= @$server->set($wrapped_key,$value,0,(int)$timeout);
            if($this_cache){
                $done=TRUE;
            }
        }
        if(!$done){
            Kernelius_Logger::log("MEMCACHE SERVERS ARE DOWN");
        }


        return true;
    }


    /**
     * retrieve key
     * @param string $key
     * @return data
     */
    public function get($key, $newExpiryDate=NULL){
        $wrapped_key=$this->wrapKey($key);
        // refresh age
        $value=FALSE;
        $foundInMaster=true;
        foreach($this->_getKeyLocation($wrapped_key) as $num => $server){
            Kernelius_Logger::log("get $key($wrapped_key) from cache server($num) {$this->_server_config[$num]["host"]}:{$this->_server_config[$num]["port"]} ");
            $value= @$server->get($wrapped_key);// or Kernelius_Logger::log("ERROR SAVING $key to log");
            if($value!==FALSE){
                break;
            }else{
                Kernelius_Logger::log("key $key($wrapped_key) not found in memcache($num)");
                $foundInMaster=false;
            }
        }

        //key not found
        if(!$value){
            Kernelius_Logger::log("key $key($wrapped_key) not found in SLAVE memcache");
            return NULL;
        }

        //key found and want to refresh
        if(!$newExpiryDate and !$foundInMaster){
            $this->restoreKeyInMaster($wrapped_key,$value, Kernelius_Cache::DEFAULT_EXPIRY);
        }else if($newExpiryDate){
            $this->set( $key, $value,$newExpiryDate);
        }
        Kernelius_Logger::log("memcache $key($wrapped_key) => $value");
        return $value;
    }


    /**
     * save data to master cache
     * @param string $key
     * @param data $value
     * @param int $timeout
     */
    protected function restoreKeyInMaster($key,$value,$timeout=Kernelius_Cache::DEFAULT_EXPIRY){
        $servers= $this->_getKeyLocation($key);
        $master=array_shift($servers);
        @$master->set($key,$value,0,(int)$timeout);// or Kernelius_Logger::log("error saving $key to master");
    }


    /**
     * delete one key from cache servers
     * @param string $key to delete
     */
    public function delete($key){
        $wrapped_key=$this->wrapKey($key);
        foreach($this->_getKeyLocation($wrapped_key) as $num => $server){
            Kernelius_Logger::log("delete $key($wrapped_key) from cache server($num) {$this->_server_config[$num]["host"]}:{$this->_server_config[$num]["port"]}");
            @$server->delete( $wrapped_key);
        }
    }



    /**
     * return array with main and backup servers for one key
     * @param string $key
     * @return array
     */
    protected function _getKeyLocation($key){
        $ret=array();
        $int_key= ord( substr( $key,-1));
        $flat_key= $int_key? $int_key : 1;
        $main_server_id= $this->mainServerForKey( $flat_key);
        $backup_server_id= $this->backupServerForKey($flat_key);
        if($main_server_id){
            $ret[$main_server_id]= $this->_getServer($main_server_id);
        }

        // if backup server id is the same as main, we don't write 2 times
        if( !empty($backup_server_id) and $backup_server_id!= $main_server_id){
            $ret[$backup_server_id]= $this->_getServer($backup_server_id);
        }
        return $ret;
    }


    /**
     * return main server id for one flat key
     * @param int $i
     * @return int
     */
    protected function mainServerForKey($i){

        $server= $this->countServers() ? $i % $this->countServers()+1 : -1;
//        Kernelius_Logger::log($server);
        return $server;
    }


    /**
     * return backup server id for one key
     * @param int $i
     * @return
     */
    protected function backupServerForKey($i){

        for( $offset=1; $offset<= $this->countServers(); $offset++){
            if( $this->isUp( $this->mainServerForKey($i+$offset) ) ){
                return $this->mainServerForKey($i+$offset);
            }
        }
        return $this->mainServerForKey($i);
    }


    /**
     *  return memcache server object
     * @param int $i
     * @return memcache
     */
    protected function _getServer($i){
        return $this->_server[(int)$i];
    }


    /**
     * server id is up?
     * @param int
     * @return bool
     */
    public function isUp($server=-1){
        if($server==-1){
            foreach($this->_server_up as $status ){
                if($status){
                    return true;
                }
            }
        }
        return isset($this->_server_up[$server]) ? $this->_server_up[$server]:NULL;
    }


    /**
     *
     * @return int total num of servers
     */
    protected function countServers(){
        return count($this->_server);
    }


    /**
     * wraps key, memcached accepts limited size for keys
     * @param string $key
     * @return string
     */
    protected function wrapKey($key){
        return md5($key);
    }


    /**
     * set cache as session save handler
     */
    public function setSaveHandler(){
        ini_set("session.save_handler","user");
        session_set_save_handler(
            array($this, 'open'),
            array($this, 'close'),
            array($this, 'read'),
            array($this, 'write'),
            array($this, 'destroy'),
            array($this, 'gc')
        );
    }


}
